package repository

import (
	"fmt"
	"log"

	"happy-sudoku/internal/codec"
	"happy-sudoku/internal/model"
	"happy-sudoku/internal/util"

	"github.com/go-redis/redis"
	"github.com/pkg/errors"
)

// IRoomRepo is to be impl by RoomRepo
type IRoomRepo interface {
	Create(room model.Room) (*model.Room, error)
	Delete(roomID int) error
	GetOne(id int) (*model.Room, error)
	GetAll() ([]*model.Room, error)
	GetList(ids []int) ([]*model.Room, error)
	PushPlayer(room *model.Room, playerIDs ...int) error
	PopPlayer(room *model.Room, playerIDs ...int) error
	UpdateSetting(room *model.Room, setting model.RoomSetting) error
	UpdateRemainTime(room *model.Room, t model.TimeDuration) error
	UpdateHostID(room *model.Room, hostID int) error
	UpdatePuzzleID(room *model.Room, puzzleID int) error
	UpdateRoomStatus(room *model.Room, status model.RoomStatus) error
}

// RoomRepo defines repo
// This repo use redis as data layer
// Use key-val "roomIDPool" to generate id
// Use Set "roomsSet" to save all room id
// Use Set "room:id:players" to repensent list of Players
// use Hash "room:id" to repesent room structure
// The "room:id" will have these fields
// 	- data: <json> of room
//  - remainTime: str for RemainTime (need to modify most)
//  - setting: <json> of RoomSetting
type RoomRepo struct {
	rdb *redis.Client
}

// NewRoomRepo new room repo
func NewRoomRepo(rdb *redis.Client) *RoomRepo {
	return &RoomRepo{
		rdb: rdb,
	}
}

const (
	keyRoomsSet   = "roomsSet"
	keyPlayers    = "players"
	keyRoom       = "room"
	keyRoomIDPool = "roomIDPool"
)

const (
	hFieldData    = "data"
	hFieldTime    = "remainTime"
	hFieldSetting = "setting"
)

// keyRoomID returns "room:id"
func keyRoomID(id int) string {
	return fmt.Sprintf("%s:%d", keyRoom, id)
}

// keyRoomIDPlayers returns "room:id:players"
func keyRoomIDPlayers(id int) string {
	return keyRoomID(id) + ":" + keyPlayers
}

// genRoomID will generate unique and available id
func (r *RoomRepo) genRoomID() (int, error) {
	id, err := r.rdb.Incr(keyRoomIDPool).Result()
	return int(id), (err)
}

// Create creates RoomRepo
func (r *RoomRepo) Create(room model.Room) (*model.Room, error) {
	// Get room id first
	id, err := r.genRoomID()
	if err != nil {
		return nil, err
	}
	// change room's id
	room.ID = id
	// marsh room firstly
	roomJSON, err := room.Marshal()
	if err != nil {
		return nil, err
	}
	roomSettingJSON, err := room.Setting.Marshal()
	if err != nil {
		return nil, err
	}

	// set to redis
	pipe := r.rdb.Pipeline()
	// add to players (room:id:players)
	pipe.SAdd(keyRoomIDPlayers(id), util.IntToInterfaceSlice(room.PlayersID)...)
	// set roomHash (room:id)
	pipe.HMSet(keyRoomID(id), map[string]interface{}{
		hFieldData:    roomJSON,
		hFieldTime:    util.IntToString(int(room.RemainTime)),
		hFieldSetting: roomSettingJSON,
	})
	// add to roomSet (roomsSet)
	pipe.SAdd(keyRoomsSet, id)
	// execute
	_, err = pipe.Exec()

	if err != nil {
		// check error
		// rollback
		derr := r.Delete(id)
		if derr != nil {
			log.Println("Warning", derr)
		}
		return nil, errors.Wrapf(err, "room create failed")
	}

	return &room, nil
}

// Delete RoomRepo
func (r *RoomRepo) Delete(roomID int) error {
	pipe := r.rdb.Pipeline()
	// delete from roomsSet
	pipe.SRem(keyRoomsSet, roomID)
	// delete room:id:players and room:id
	pipe.Del(keyRoomIDPlayers(roomID), keyRoomID(roomID))
	_, err := pipe.Exec()

	return errors.Wrapf(err, "RoomRepo.Delete")
}

func (r *RoomRepo) checkRoomExist(id int) error {
	// check whether id is in room
	isMem, err := r.rdb.SIsMember(keyRoomsSet, id).Result()
	if err != nil {
		return errors.Wrap(err, "RoomRepo.CheckRoomExist")
	}
	if !isMem {
		return codec.ErrRoom404.WithField("id", id)
	}
	return nil
}

// GetOne room
func (r *RoomRepo) GetOne(id int) (*model.Room, error) {
	const errHeader = "RoomRepo.GetOne"

	err := r.checkRoomExist(id)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}
	// Room do exist now
	room := &model.Room{}

	var roomHashDataCmd *redis.StringCmd
	var roomHashTimeCmd *redis.StringCmd
	var roomHashSettingCmd *redis.StringCmd
	var playersCmd *redis.StringSliceCmd
	// Pipe
	pipe := r.rdb.Pipeline()
	// Get room data from hash <json>
	roomHashDataCmd = pipe.HGet(keyRoomID(id), hFieldData)
	roomHashSettingCmd = pipe.HGet(keyRoomID(id), hFieldSetting)
	roomHashTimeCmd = pipe.HGet(keyRoomID(id), hFieldTime)
	// Get players
	playersCmd = pipe.SMembers(keyRoomIDPlayers(id))
	_, err = pipe.Exec()

	if err != nil {
		return nil, errors.Wrapf(err, errHeader)
	}

	// convert from json
	err = room.Unmarshal(roomHashDataCmd.Val())
	if err != nil {
		return nil, errors.Wrapf(err, errHeader)
	}
	// set setting
	setting := &model.RoomSetting{}
	err = setting.Unmarshal(roomHashSettingCmd.Val())
	if err != nil {
		return nil, errors.Wrapf(err, errHeader)
	}
	room.Setting = setting
	// set time
	dur, err := util.StringToInt(roomHashTimeCmd.Val())
	if err != nil {
		return nil, errors.Wrapf(err, errHeader)
	}
	room.RemainTime = model.TimeDuration(dur)

	// conver playersID
	players := playersCmd.Val()
	playersIDs, err := util.StringToIntSlice(players)
	if err != nil {
		return nil, errors.Wrap(err, errHeader)
	}
	room.PlayersID = playersIDs

	return room, nil
}

// GetList return list of room given id
func (r *RoomRepo) GetList(ids []int) ([]*model.Room, error) {
	rooms := []*model.Room{}

	var room *model.Room
	var err error
	for _, id := range ids {
		room, err = r.GetOne(id)
		if err != nil {
			continue
		}
		// save only no err
		rooms = append(rooms, room)
	}

	// !! Warning: Ignore all error
	// TODO: Find a way to log error
	return rooms, nil
}

// GetAll get all rooms
func (r *RoomRepo) GetAll() ([]*model.Room, error) {
	// Get all room id
	idStrs, err := r.rdb.SMembers(keyRoomsSet).Result()
	if err != nil {
		return nil, errors.Wrap(err, "get all room")
	}
	ids, err := util.StringToIntSlice(idStrs)
	return r.GetList(ids)
}

// PushPlayer pushs player in specific room
func (r *RoomRepo) PushPlayer(room *model.Room, playerIDs ...int) error {
	const errHeader = "RoomRepo.PushPlayer"

	err := r.checkRoomExist(room.ID)
	if err != nil {
		return errors.Wrap(err, errHeader)
	}
	// push player
	// update player
	var playersCmd *redis.StringSliceCmd
	pipe := r.rdb.Pipeline()
	pipe.SAdd(keyRoomIDPlayers(room.ID), util.IntToInterfaceSlice(playerIDs)...).Err()
	playersCmd = pipe.SMembers(keyRoomIDPlayers(room.ID))
	_, err = pipe.Exec()

	if err != nil {
		return errors.Wrap(err, errHeader)
	}

	players, err := util.StringToIntSlice(playersCmd.Val())
	if err != nil {
		return errors.Wrap(err, errHeader)
	}
	room.PlayersID = players

	return err
}

// PopPlayer pops player from specific room
func (r *RoomRepo) PopPlayer(room *model.Room, playerIDs ...int) error {
	const errHeader = "RoomRepo.PopPlayer"

	err := r.checkRoomExist(room.ID)
	if err != nil {
		return errors.Wrap(err, errHeader)
	}
	// pop player
	// update player
	var playersCmd *redis.StringSliceCmd
	pipe := r.rdb.Pipeline()
	pipe.SRem(keyRoomIDPlayers(room.ID), util.IntToInterfaceSlice(playerIDs)...).Err()
	playersCmd = pipe.SMembers(keyRoomIDPlayers(room.ID))
	_, err = pipe.Exec()

	if err != nil {
		return errors.Wrap(err, errHeader)
	}

	players, err := util.StringToIntSlice(playersCmd.Val())
	if err != nil {
		return errors.Wrap(err, errHeader)
	}
	room.PlayersID = players

	return err
}

// UpdateSetting updates setting
func (r *RoomRepo) UpdateSetting(room *model.Room, setting model.RoomSetting) error {
	const errHeader = "RoomRepo.UpdateSetting"
	// no need to check, just update
	data, err := setting.Marshal()
	if err != nil {
		return errors.Wrap(err, errHeader)
	}
	err = r.rdb.HSet(keyRoomID(room.ID), hFieldSetting, data).Err()
	if err != nil {
		return err
	}
	// update room setting
	room.Setting = &setting
	return nil
}

// UpdateRemainTime updates time
func (r *RoomRepo) UpdateRemainTime(room *model.Room, t model.TimeDuration) error {
	durStr := util.IntToString(int(t))
	// no nead check, just update
	err := r.rdb.HSet(keyRoomID(room.ID), hFieldTime, durStr).Err()
	if err != nil {
		return err
	}
	// update time
	room.RemainTime = t
	return nil
}

// UpdateHostID updates room's host
func (r *RoomRepo) UpdateHostID(room *model.Room, hostID int) error {
	const errHeader = "RoomRepo.UpdateHostID"

	room.HostID = hostID
	err := r.updateRoomData(room)
	return errors.Wrap(err, errHeader)
}

// UpdatePuzzleID updates room's puzzleID
func (r *RoomRepo) UpdatePuzzleID(room *model.Room, puzzleID int) error {
	const errHeader = "RoomRepo.UpdatePuzzleID"

	room.PuzzleID = puzzleID
	err := r.updateRoomData(room)
	return errors.Wrap(err, errHeader)
}

// UpdateRoomStatus updates room's status
func (r *RoomRepo) UpdateRoomStatus(room *model.Room, status model.RoomStatus) error {
	const errHeader = "RoomRepo.UpdateRoomStatus"

	room.Status = status
	err := r.updateRoomData(room)
	return errors.Wrap(err, errHeader)
}

// updateRoomData updates room raw data
func (r *RoomRepo) updateRoomData(room *model.Room) error {
	err := r.checkRoomExist(room.ID)
	if err != nil {
		return err
	}

	// Set Room
	roomJSON, err := room.Marshal()
	if err != nil {
		return err
	}

	// insert
	err = r.rdb.HSet(keyRoomID(room.ID), hFieldData, roomJSON).Err()
	return err
}
